﻿using System;
using System.Linq.Expressions;

namespace Data.Core.Specifications
{
    public static class SpecificationExtensions
    {
        public static ISpecification<TEntity> And<TEntity>(this ISpecification<TEntity> self, ISpecification<TEntity> other)
        {
            var lambda = Combine(self.Predicate, other.Predicate, Expression.AndAlso);

            return new Specification<TEntity>(lambda);
        }

        public static ISpecification<TEntity> Or<TEntity>(this ISpecification<TEntity> self, ISpecification<TEntity> other)
        {
            var lambda = Combine(self.Predicate, other.Predicate, Expression.OrElse);

            return new Specification<TEntity>(lambda);
        }

        static Expression RewriteLambdaBody(LambdaExpression expression, ParameterExpression parameter)
        {
            return new ParameterRewriter(expression.Parameters[0], parameter).Visit(expression.Body);
        }

        private static Expression<Func<TEntity, bool>> Combine<TEntity>(Expression<Func<TEntity, bool>> left, Expression<Func<TEntity, bool>> right, Func<Expression, Expression, Expression> expression)
        {
            ParameterExpression parameter = Expression.Parameter(left.Parameters[0].Type, left.Parameters[0].Name);
            var body = expression(RewriteLambdaBody(left, parameter), RewriteLambdaBody(right, parameter));
            var lambda = Expression.Lambda<Func<TEntity, bool>>(body, parameter);
            return lambda;
        }

        class ParameterRewriter : ExpressionVisitor
        {

            readonly ParameterExpression candidate;
            readonly ParameterExpression replacement;

            public ParameterRewriter(ParameterExpression candidate, ParameterExpression replacement)
            {
                this.candidate = candidate;
                this.replacement = replacement;
            }

            protected override Expression VisitParameter(ParameterExpression expression)
            {
                return ReferenceEquals(expression, candidate) ? replacement : expression;
            }
        }
    }
}
